Skip to main content

05 - 供应链复盘:安全扫描器成了攻击入口

前置03 - 工具投毒与 Rug Pull 里"不固定版本"的风险。

本篇回答:一次完整的、真实的 Agent 基础设施攻击,从头到尾是怎么发生的?

会用到的词

  • 供应链攻击:不打你,打你依赖的东西
  • .pth 文件:Python 的一个特性 —— 放在 site-packages/ 下、以 import 开头的 .pth 文件,会在每次 Python 解释器启动时被自动执行
  • IOC(Indicator of Compromise):入侵指标,比如恶意域名、文件哈希

一、这次事件的特殊之处

04 篇的结论是"网关是最好的防线位置"。这一篇讲的是那道防线自己被打穿的那一天

被打的是 BerriAI/litellm —— 本站 Agent 网关专题拆了整整十篇的那个项目,★56,709,月下载量约 9,500 万。

它是很多公司唯一持有全部模型 API 凭证的组件。

二、时间线

LiteLLM 供应链攻击 · 2026 年 3 月03-23注册外传域名litellm.cloud03-24 10:39恶意包上架 PyPI1.82.7 / 1.82.803-24 13:48官方 issue 建立#2451803-25社区贡献扫描脚本03-26补充入侵指标03-27官方安全说明会03-301.83.0 发布全新 CI/CD v2恶意包在架约 40 分钟从投毒到发布干净版本,六天。这个响应速度在开源项目里已经算快的了。但对凭证窃取型攻击来说,图上那条深色的 40 分钟已经决定了一切 —— 后面六天做的都是止损,不是止血。
时间线上真正该盯的不是总时长,是「上架」到「被发现」之间那一段。凭证一旦被取走,把包下架并不能让它失效。

从投毒到发布干净版本,六天。 这个响应速度在开源项目里已经算快的了 —— 但对于一个凭证窃取型攻击来说,前 40 分钟就已经决定了一切

三、攻击链

这是整件事最值得反复讲的部分。

攻击链 —— 注意入口是那个用来做安全扫描的工具本身① Trivy 被投毒一个开源安全扫描器,跑在LiteLLM 的 CI 里② 凭证被窃取拿到 maintainer的 PyPI 发布权限③ 绕过 CI 发包GitHub Release只到 v1.82.6.dev1两个恶意版本没走 CI④ 全球用户安装月下载量约 9,500 万⑤ 凭证批量外传送往攻击者七天前就注册好的域名这件事最辛辣的地方在第 ① 步:被投毒的是流水线里那个负责「做安全扫描」的工具。你的依赖清单里,安全工具和业务依赖享有同样的信任级别,而它通常还拥有更高的权限。
把这条链和 03 篇的工具投毒放在一起看:一个发生在运行时的工具描述里,一个发生在构建期的依赖里,但结构完全一样 —— 都是往一条你默认可信的通道里塞东西。

官方 issue 里 LiteLLM 团队的原话:

Compromise came from trivvy security scan dependency

攻击者没有打 LiteLLM,他们打的是 LiteLLM 用来做安全检查的那个工具。

这件事的讽刺程度值得停下来想一想:你在 CI 里加一个安全扫描器,是为了提高安全性。而这个扫描器本身成了攻击入口 —— 它天然拥有读取仓库、访问 CI 密钥的权限。

💡 可以直接拿走的教训:CI 里的每一个工具都是你的攻击面,尤其是那些需要高权限的安全工具。它们通常被无条件信任,因为"它是来保护我们的"。

3.1 一个关键的辨识信号

issue 里写得很清楚:

The attacker published malicious versions to PyPI that were never released through the official GitHub CI/CD. GitHub releases only go up to v1.82.6.dev1 — versions 1.82.7 and 1.82.8 on PyPI were uploaded directly by the attacker.

PyPI 上的版本号,比 GitHub Release 上的版本号更新。 这是一个任何人都能发现的异常信号。

能拿走的自查方法:定期比对关键依赖在包仓库和源码仓库上的版本一致性。这是可以自动化的,而且几乎零成本。

四、Payload 的两个版本

版本投毒方式触发条件
1.82.7payload 藏在 litellm/proxy/proxy_server.pyimport litellm.proxy
1.82.8新增 litellm_init.pth(34,628 字节)+ proxy_server.py 里的 payload任意 Python 进程启动 —— 完全不需要 import

4.1 .pth 文件为什么致命

Python 有一个鲜为人知的特性:site-packages/ 目录下的 .pth 文件,如果某一行以 import 开头,这一行会在解释器启动时被自动执行

它的本意是让包能在启动时做一些路径注册,但后果是:

  • 你不需要 import litellm
  • 你甚至不需要用到 LiteLLM
  • 只要这个包装在你的环境里,你在这个环境里跑任何一行 Python,payload 就执行了
# 这些全都会触发
python -c "print(1)"
pytest
pip list
你项目里任何一个和 litellm 毫无关系的脚本

从 1.82.7 到 1.82.8,攻击者做的这一步升级,把感染面从"用了这个库的进程"扩大到了"装了这个库的整个环境"。

4.2 窃取的内容

官方 issue 的原文清单:

  1. Collects: SSH keys, environment variables (API keys, secrets), AWS/GCP/Azure/K8s credentials, crypto wallets, database passwords, SSL private keys, shell history, CI/CD configs
  2. Encrypts: AES-256-CBC + RSA-4096 (hardcoded public key)
  3. Exfiltrates: curl POST to https://models.litellm.cloud/

三点观察:

1. 目标不是 LLM 相关的东西,是整台机器上的一切凭证。 SSH 私钥、云凭证、K8s secret、数据库密码、SSL 私钥、shell history —— 一次得手,你的整个基础设施都暴露了。

2. 加密外传(AES-256-CBC + 硬编码 RSA-4096 公钥)。 这不是为了保护你的数据,是为了让流量审计看不出传的是什么,同时保证只有攻击者能解密。这是成熟的攻击工具,不是脚本小子。

3. 外传域名 models.litellm.cloud 注意 —— 官方域名是 litellm.ai,攻击者用的是 litellm.cloud

这个域名注册于 2026-03-23,比恶意包上架早了几个小时。

一个只看域名字符串"眼熟不眼熟"的人,很难在告警里把它挑出来。出站域名白名单必须是白名单(只放行已知的),不能是黑名单或者"看着像就放行"。 这正好呼应 04 篇里那条"出站域名白名单"的建议。

五、未受影响的部署形态

官方的这条说明是整个事件里最有价值的一句:

Proxy Docker image users were not impacted, all dependencies are pinned on requirements.txt

用官方 Docker 镜像的用户毫发无损,因为镜像里的依赖是钉死版本的。

把这条和 Agent 网关 · 02 - 四种形态对照着看:

部署形态这次的结局
pip install litellm⚠️ 中招,且 .pth 让整个 Python 环境暴露
官方 Docker 镜像跑 Proxy✅ 未受影响

同一个软件、同一个漏洞,两种形态的后果完全不同。

而且注意方向:作为引入是最脆弱的 —— payload 直接落进了你的业务进程所在的环境,能拿到那台机器上的一切。作为独立进程(容器)跑,爆炸半径被限制在容器内,而且钉死的版本从一开始就挡住了恶意版本。

这也验证了 03 篇里 SkillSpector Rug Pull 检测器那几条规则的价值 —— 它们检查的就是"有没有钉版本":

remediation="Pin the version: pip install package==1.2.3"

一条几十年历史的、无聊的、跟 AI 毫无关系的工程实践,在这次事件里是唯一有效的防线。

六、攻击者对社区讨论的干扰

issue 里有这么一段:

Attacker behavior: The attacker appears to be publishing hundreds of spam comments to suppress discussion.

原始的技术分析 issue #24512 被攻击者用垃圾评论刷到关闭

攻击的一部分是让受害者更晚发现。 40 分钟的窗口期里,能拖延一分钟就多一分钟的收获。

这对应急响应的启示是:事故沟通渠道本身要有备份。 LiteLLM 团队当时建议大家转到 Hacker News 的讨论串。如果你的唯一沟通渠道是攻击者也能写入的地方,那它在事故中不可靠。

七、官方做了什么

从 issue 里摘出的处置动作:

动作说明
删除恶意包v1.82.7、v1.82.8
轮换全部 maintainer 账号新账号 @krrish-berri-2@ishaan-berri
暂停发版直到确认供应链干净
引入外部力量官方称已联系 Google Mandiant 团队
排查影响面审查所有 berriai 仓库、扫描 CircleCI 构建
重建 CI/CD03-30 发布 1.83.0,走全新的 CI/CD v2 流水线
公开说明会03-27 举行 Security Townhall

PyPI 侧的反应更激烈:整个 litellm 包一度被下架,所有版本都返回 "No matching distribution found"。

💡 注意这条对你的影响:包被下架的那段时间里,所有钉了旧版本、且没有本地缓存的构建全部失败。也就是说,即使你没中招,你的 CI 也会挂。

事故响应计划里要包含这一条:上游包被下架时,你还能不能构建? 私有镜像源在这时候是唯一的答案。

八、这次事件对应 OWASP 的哪一条

01 篇的 ASI 清单里,这是 ASI04 — Agentic Supply Chain Compromise 的教科书案例。

但它同时命中了另外两条:

  • ASI03(身份与权限滥用):拿到的是 maintainer 的发布权限
  • ASI05(意外的代码执行).pth 让代码在完全没预期的时机执行

真实事故从来不只对应一个条目。 威胁清单的用途是排查,不是分类。

九、七项应对措施

#动作成本
1所有依赖钉死版本,用 lock 文件
2AI 网关用容器跑,别当库 import
3自动比对关键依赖的 PyPI / npm 版本与 GitHub Release 版本
4出站域名白名单 —— 只放行已知域名
5审计 CI 里的每一个工具,尤其是安全工具
6搭私有镜像源,保证上游下架时还能构建
7演练一次"全部凭证轮换",测出实际需要多久

第 7 条最容易被跳过,也最重要。这次事故的官方建议是 rotate ALL credentials —— 如果你从来没演练过,真出事那天你会发现有些凭证根本不知道存在哪、谁在用、改了会挂掉什么。

十、全专题结论

问题答案
Agent 安全的根本难点是什么?指令和数据在同一个上下文里,提示注入挡不住(01
那怎么办?不追求"不被注入",追求"被注入了也干不了大事"
协议层在做什么?把信任边界画得更细:角色分离、令牌绑定、iss 校验(02
攻击具体长什么样?隐藏注释、零宽字符、同形异义字、未固定版本(03
防线放哪儿?网关是唯一同时看得见身份和意图的位置(04
网关自己被打穿呢?钉版本、用容器、白名单出站(本篇)

一条贯穿全专题的主线:这个领域里真正有效的防御,几乎都不是 AI 技术,而是几十年的老工程实践 —— 最小权限、默认拒绝、钉版本、白名单、职责分离。

新的是攻击面,不是解法。

← 回到 专题索引  ·  Agent Infra 板块总览